/**
 * Created by Richard Radlherr.
 * Date: 26.06.2006 22:18
 */

using System;
using TagTool.Core;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;

namespace TagTool.Core
{
	/**
	 * Tha ProjectManager handles activities belonging to a
	 * Tagtool project file. That means it exposes the structure of a 
	 * project in terms of "scenes", "nested nodes" "nodes" and "slots"
	 * which are the basic concepts of the Tagtool.
	 * The Projectmanager is responsible for loading and saving Tagtool Projects
	 * and also for creation of arbitrary nodes. Therfore - when loading an node - 
	 * the name of the Assembly in which the expected node resides is a requred argument.
	 * Naturally the name of the node itself must be given to determine the identity of the 
	 * required node.
	 * 
	 * TODO: specify "scenes" as lists of nodes
	 * TODO: specify "nested nodes" as list of nodes with exposed slots
	 * Q: -> is a scene a simple "nested node" ?
	 */
	public class ProjectManager : StageManager
	{
		/**
		 * The nodes the project contains
		 */
		private SortedList<int, BaseNode> nodes = new SortedList<int, BaseNode>();
		
		private List<IStage> stages = new List<IStage>();
		
		// the connection to the user Interface
		private INodeUIController ui;
		
		/**
		 * Get The instance of the NodeFactory
		 */
		private NodeFactory nodeFactory = NodeFactory.Instance;
		
		/**
		 * First the mamanager needs to be initialized with an 
		 * UI- Controller - interface which is used to let the
		 * node- system communicate with the ui via the ProjectManager.
		 * So indepndence of the nodes from their representation in
		 * the user interface is achieved.
		 */
		public static ProjectManager GetInstance(INodeUIController ui)
		{
			if (manager == null)
			{
				manager = new ProjectManager(ui);
			}
			return manager;
		}
		
		public INodeUIController UI
		{
			get { return ui; }
		}
		
		private static ProjectManager manager;
		
		private String appDir;
		
		private ProjectManager()
		{
			appDir = System.IO.Directory.GetCurrentDirectory();
		}
		
		public String AppDir
		{
			get { return appDir; }
		}

		private ProjectManager(INodeUIController ui)
		{
			appDir = (String)System.IO.Directory.GetCurrentDirectory();
			this.ui = ui;
		}
		
		public String SessionName
		{
			get { return ui.SessionName; }
		}
			
		
		public static ProjectManager Instance
		{
			get
			{
				if (manager == null)
				{
					manager = new ProjectManager();
				}
				return manager;
			}
		}
		
		/**
		 * Clears the NodeList and resets the NodeID- Counter.
		 */
		public void New()
		{
			nodes.Clear();
			BaseNode.ResetIDCounter();
		}
		
		private void CreateNodeUI(INodeUIController c, List<ConnectionInfo> connections)
		{
			foreach(KeyValuePair<int, BaseNode> b in nodes)
	        {
				c.AddNode(b.Value);
	        }
			
			foreach (ConnectionInfo i in connections)
	        {
	        	i.CreateConnection(c);
	        }
		}
		
		/**
		 * private class used for restoring of the connection of nodes.
		 */
		private class ConnectionInfo
		{
			public InputSlot inp;
			public int nodeId;
			public String slotId;
			public ConnectionInfo(InputSlot inp, int nodeId, String slotId)
			{
				this.inp = inp;	
				this.nodeId = nodeId;
				this.slotId = slotId;
			}
			
			public void CreateConnection(INodeUIController c)
			{
				BaseNode node = ProjectManager.Instance.GetNode(nodeId);
				PropertyInfo[] props = node.GetType().GetProperties();

				foreach(PropertyInfo prop in props)
				{
					object[] atts;
			        atts = prop.GetCustomAttributes(typeof(NodeAttribute), true);
			        if (atts.GetLength(0) != 0)
			        {
			        	NodeAttribute    att = (NodeAttribute) atts[0];
						if (att.SlotType == SlotType.Output)
						{
							OutputSlot outputSlot = (OutputSlot)prop.GetValue(node, null);
							if (outputSlot.Name.Equals(slotId))
							{
								c.ConnectSlots(inp, outputSlot);
							}
						}
			        }
			
				}
			}
		}
		
		/**
		 * Loads a Project file with a given name and initializes 
		 * all nodes contained in the project.
		 * Rules for loading:
		 *  1.) Create the nodes as their names appears in the save- file.
		 *  2.) For each InputSlot which is NOT connected to to another node restore
		 *      the default value.
		 * 3.) For each InputSlot which IS connected to another node restore the connection
		 *     beween them.
		 */
		public void Load(String fileName, INodeUIController c)
		{
			New();
			List<ConnectionInfo> connections = new List<ConnectionInfo>();
			
	        Stream stream = File.Open(fileName, FileMode.Open);
	        BaseNode node;
	        do
	        {
	        	node = LoadNode(stream, connections);
	        }
	        while (stream.Position < stream.Length);
	        
	        CreateNodeUI(c, connections);
	        
	        foreach(KeyValuePair<int, BaseNode> b in nodes)
	        {
	        	b.Value.Initialize();
	        }
	        
	        stream.Close();
		}
		
		/**
		 * Saves a Project file with a given name 
		 * Rules for saving:
		 *  0.) TODO: specify scene saving behavior
		 *  1.) For Nodes Store Assembly name and Node Name and a unique 
		 *      Project- wide identifier of tht node (that is the key of the node)
		 *  2.) For each InputSlot which is NOT connected to another node 
		 * 		store the actual Value (-> all types which are used as Value- Types
		 *      must therfore be serializable (when XML is chosen fpr output to a file ???)
		 *  3.) For each InputSlot which IS connected to another node store the 
		 *      ID of the connected Node and the name of the slot where the connection is bound
		 */
		public void Save(String fileName)
		{
	        Stream stream = File.Open(fileName, FileMode.Create);
	        foreach(KeyValuePair<int, BaseNode> b in nodes)
	        {
	        	SaveNode(stream, b.Value);
	        }
	        
	        stream.Close();
		}
		
		/**
		 * returns the actual ammout of nodesused in the project
		 */
		public int NodeCount
		{
			get { return nodes.Count; }
		}

		/**
		 * Returns the number of scenes in the project
		 */
		public int SceneCount
		{
			get { return 0; }
		}
		
		/**
		 * Creates a Node with given Assemblyname and nodeType name.
		 */
		public BaseNode CreateNode(String assembly, String nodeTypeName)
		{
			BaseNode node = nodeFactory.CreateNode(assembly, nodeTypeName);
			if (node != null)
			{
				nodes.Add(node.Id, node);
			}
			return node;
		}
		
		/**
		 * Creates a Node with given Assemblyname and nodeType name.
		 */
		public BaseNode CreateNode(String assembly, String nodeTypeName, int id)
		{
			BaseNode node = nodeFactory.CreateNode(assembly, nodeTypeName);
			if (node != null)
			{
				node.Id = id;
				nodes.Add(node.Id, node);
			}
			return node;
		}
		
		/**
		 * Removes the node with the given Id from the node list
		 */
		public void RemoveNode(int id)
		{
			if (nodes.ContainsKey(id))
			{
				nodes.Remove(id);
			}
		}
		
		/**
		 * Removes the given node
		 */
		public void RemoveNode(BaseNode node)
		{
			nodes.Remove(node.Id);
		}
		
		/**
		 * return the node with the given key.
		 */
		public BaseNode GetNode(int id)
		{
			if (nodes.ContainsKey(id))
			{
				return nodes[id];
			}
			return null;
		}
		
		/**
		 * Adds a given Stage
		 */
		public void AddStage(IStage stage)
		{
			if (!stages.Contains(stage))
			{
				stages.Add(stage);
				GC.Collect();
			}
		}
		
		public IStage CreateStage(String name)
		{
			IStage stage = ui.CreateStage(name);
			AddStage(stage);
			return stage;
		}
		
		/**
		 * Removes the given stage
		 */
		public void RemoveStage(IStage stage)
		{
			stages.Remove(stage);
		}
		
		/**
		 * returns the number of active stages
		 */
		public int StageCount
		{
			get { return stages.Count; }
		}
		
		/**
		 * Return the stage with the given index.
		 */
		public IStage GetStage(int index)
		{
			return stages[index];
		}
		
		/**
		 * Saves a specific node into an opened Stream at the current
		 * Stream- position.
		 */ 
		private void SaveNode(Stream stream, BaseNode node)
		{
			StreamWriter writer = new StreamWriter(stream);
			writer.WriteLine("{0}:{1}{2}:{3}:{4}:{5}", node.Id, node.GetType().Assembly.GetName().Name, ".dll", node.GetType().FullName, node.LocX, node.LocY);
			writer.Flush();
	
			BinaryFormatter b = new BinaryFormatter();
        	
			PropertyInfo[] props = node.GetType().GetProperties();

			foreach(PropertyInfo prop in props)
			{
				object[] atts;
		        atts = prop.GetCustomAttributes(typeof(NodeAttribute), true);
		        if (atts.GetLength(0) != 0)
		        {
		            NodeAttribute    att = (NodeAttribute) atts[0];
					if (att.SlotType == SlotType.Input)
					{
						// - slotname
						InputSlot inputSlot = (InputSlot)prop.GetValue(node, null);
						writer.WriteLine(inputSlot.Name);
						writer.Flush();
						
						// is Active ?
						writer.WriteLine("Active: {0}", inputSlot.Active);
						writer.Flush();
						
						// connection
						if (inputSlot.Connected)
						{
							writer.WriteLine("Connection: {0}:{1}", inputSlot.ConnectedNode.Id, inputSlot.Input.Name);
							writer.Flush();
						}
						else
						{
							writer.WriteLine("Unconnected");
						}
						writer.Flush();
						
						// Defaultvalue
						writer.WriteLine("Defaultvalue:");
						writer.Flush();
						Object ser = inputSlot.DefaultValue;
						b.Serialize(stream, ser);
						writer.WriteLine();
		            	writer.Flush();
					}
		            else
		            {
		            	OutputSlot outputSlot = (OutputSlot)prop.GetValue(node, null);
		            	writer.WriteLine(outputSlot.Name);
						writer.Flush();
	            		Object ser = outputSlot.Value;
						b.Serialize(stream, ser);
		            	writer.WriteLine();
		            	writer.Flush();
		            }
		        }
			}
		}

		/**
		 * Helper method: read an Integer value from the stream
		 * until lastChar appears. the laschar is being skipped.
		 */
		private int ReadInt(Stream stream, char lastChar)
		{
			String s = ReadString(stream, lastChar);
			return Convert.ToInt32(s);
		}

		/**
		 * Helper method: read a String value from the stream
		 * until lastChar appears. the laschar is being skipped.
		 */
		private String ReadString(Stream stream, char lastChar)
		{
			StringBuilder b = new StringBuilder();
			char ch;
			do
			{
				ch = (char)stream.ReadByte();
				if (ch != lastChar)
				{
					b.Append(ch);
				}
			}
			while (ch != lastChar);
			
			if (lastChar == (char)0x0d)
			{
				stream.ReadByte(); // skip 0x0a
			}
			
			return b.ToString();
		}

		/**
		 * Restores a node from the given stream at the current
		 * stream position.
		 */
		private BaseNode LoadNode(Stream stream, List<ConnectionInfo> connections)
		{
			BaseNode node = null;
			StreamReader reader = new StreamReader(stream);
			int id = ReadInt(stream, ':');
			String assembly = ReadString(stream, ':');
			String type = ReadString(stream, ':');
			
			node = CreateNode(assembly, type, id);
			
			node.LocX = ReadInt(stream, ':');
			node.LocY = ReadInt(stream, (char)0x0d);

			PropertyInfo[] props = node.GetType().GetProperties();

			foreach(PropertyInfo prop in props)
			{
				object[] atts;
		        atts = prop.GetCustomAttributes(typeof(NodeAttribute), true);
		        if (atts.GetLength(0) != 0)
		        {
		        	NodeAttribute    att = (NodeAttribute) atts[0];
					if (att.SlotType == SlotType.Input)
					{
						InputSlot inputSlot = (InputSlot)prop.GetValue(node, null);
						String inName = ReadString(stream, (char)0x0d);
						String active = ReadString(stream, (char)0x0d);
						if (active.Equals("Active: True"))
						{
							inputSlot.Active = true;
						}
						else
						{
							inputSlot.Active = false;
						}
						String connected = ReadString(stream, (char)0x0d);
						if (!connected.Equals("Unconnected"))
						{
							// store connection details
							String[] s = connected.Split(':');
							int nodeId = Convert.ToInt32(s[1]);
							String slotId = s[2];
							connections.Add(new ConnectionInfo(inputSlot, nodeId, slotId));
						}
						
						// read the defaultvalue
						String dv = ReadString(stream, (char)0x0d);
						
						BinaryFormatter bformatter = new BinaryFormatter();
						Object o = bformatter.Deserialize(stream);
						inputSlot.DefaultValue = o;	
						stream.ReadByte();
						stream.ReadByte();
					}
					else
					{
						OutputSlot outputSlot = (OutputSlot)prop.GetValue(node, null);
						String outName = ReadString(stream, (char)0x0d);
						BinaryFormatter bformatter = new BinaryFormatter();
						Object o = bformatter.Deserialize(stream);
						outputSlot.Value = o;	
						stream.ReadByte();
						stream.ReadByte();
					}
		        }
			}

			node.Initialize();
			return node;
		}
		
	}
}
